/**
 * Copyright 2010 Jake Jackson
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jake.nb.ext.ant.parser;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Location;
import org.apache.tools.ant.MagicNames;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.module.api.AntProjectCookie;
import org.apache.tools.ant.module.api.support.AntScriptUtils;
import org.apache.tools.ant.module.api.support.TargetLister;
import org.apache.tools.ant.taskdefs.Property;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 *
 * @author JakeJ
 */
public final class AntParser {

  public static final String STRING_DEPENDS = "depends";
  private static String[] antTaskDefs = {"ant", "antcall"};

  public static boolean isAntFile(final FileObject fileObject) {
    return fileObject != null && "text/x-ant+xml".equals(fileObject.getMIMEType());
  }

  private AntParser() {
  }

  public static int[] getClickableTarget(final Document document, final int offset) {

    // easiest way here would be parsing the text of the whole document and looking for something inside "", but
    // a) we need the exact target code anyway for the HINTs engine
    // b) we have to figure out several things, like, if we are in a comment or if the current "..." is a name
    //    declaration and so on
    int[] result = null;
    final Set<org.apache.tools.ant.module.api.support.TargetLister.Target> targets = getTargets(document);

    if (!targets.isEmpty()) {
      String text = ""; // NOI18N
      try {
        text = document.getText(0, document.getEndPosition().getOffset());
      } catch (BadLocationException ex) {
        Logger.getLogger("AntParser").log(Level.INFO, "Cannot find given location ({0})", document.getEndPosition().getOffset()); // NOI18N
      }

      // go and find the depends-tag inside the offset
      if (!text.isEmpty()) {
        for (final Iterator<TargetLister.Target> it = targets.iterator(); it.hasNext() && result == null;) {
          TargetLister.Target target = it.next();

          result = getSpanFromDepends(target, document, offset);
          if (result != null) {
            break;
          }

          for (final String string : antTaskDefs) {
            result = getSpanFromAntTask(target, document, offset, string);
            if (result != null) {
              break;
            }
          }

          // find a way to support subant
          // note, that with a subant several files can be affected

//          result = getSpanFromAntTask(target, document, offset, "subant");
//          if (result != null) {
//            break;
//          }
        }

        // check if there's really such a target or may something wrong entered in there
        // e.g. one can write: <target depends="aaa,bbb" .../>
        // but none of the targets exists or is reachable, then we must not enable linking to it
        if (result != null) {
          String name = null;
          try {
            name = document.getText(result[0], result[1] - result[0]);
          } catch (BadLocationException ex) {
            Logger.getLogger("AntParser").log(Level.INFO, "Cannot find given locations ({0}, {1})",
                    new Object[]{result[0], (result[1] - result[0])}); // NOI18N
          }
          if (name != null && name.length() > 0 && getFileForTargetName(document, name, offset) == null) {
            result = null;
          }
        }
      }
    }

    return result;
  }

  /**
   * Gets the targets contained in the document marked by the tag the given offset is located in. Or in other words,
   * gets all those targets which can be used inside a "depends" or "antcall" clause.
   * @param document the document the offset is located in currently
   * @param offset the offset
   * @return the targets possible to be used
   * @see #getUsedTargets(javax.swing.text.Document, int)
   */
  public static Set<TargetLister.Target> getPossibleTargets(final Document document, final int offset) {
    final Set<TargetLister.Target> possibleTargets = new HashSet<TargetLister.Target>();
    Collection<org.apache.tools.ant.module.api.support.TargetLister.Target> targetsToUse = null;

    final AntProjectCookie projectCookieForOffset = getProjectCookie(document, offset);
    final AntProjectCookie projectCookieForDocument = getProjectCookie(document);

    try {
      targetsToUse = TargetLister.getTargets(projectCookieForOffset);
    } catch (IOException ex) {
      Logger.getLogger("AntParser").log(Level.INFO, "Cannot list targets"); // NOI18N
    }


    if (targetsToUse != null) {
      if (projectCookieForDocument == projectCookieForOffset) {
        // we'll stay in the same document (same project cookie and no imported)
        // ok, let's iterate on the targets to find the one we are in given by the offset
        for (final TargetLister.Target target : targetsToUse) {
          if (isInsideDepends(target, document, offset) || isInsideAntcall(target, document, offset)) {
            possibleTargets.addAll(targetsToUse);
            // remove the target we are in
            possibleTargets.remove(target);
            break;
          }
        }
      } else {
        // different cookies - add all targets, 'cos it's a different build file
        possibleTargets.addAll(targetsToUse);
      }
    }
    return possibleTargets;
  }

  /**
   * Gets the description of the passed target if any.
   * @param target the target to get the description for
   * @return a string containing the description or empty if no description is present
   */
  public static String getDescription(final TargetLister.Target target) {
    String ret = "";
    if (target.isDescribed()) {
      ret = target.getElement().getAttribute("description");
    }
    return ret;
  }

  /**
   * Gets the string value of the depends clause if any.
   * @param target the target to get the string value of the depends attribute
   * @return the string value of the depends attribute or {@code null} if no depends is present
   */
  public static String getDependsClause(final TargetLister.Target target) {
    return target.getElement().hasAttribute(STRING_DEPENDS) ? target.getElement().getAttribute(STRING_DEPENDS) : null;
  }

  /**
   * Gets all the targets already used inside a "depends" clause.
   * @param document the document to look in
   * @param offset the offset to use as start point
   * @return a collection of targets already used in the depends target. Can be empty if the offset is outside the
   * "depends" clause or if there's no target used yet.
   */
  public static Set<TargetLister.Target> getUsedTargets(final Document document, final int offset) {
    final Set<TargetLister.Target> usedTargets = new HashSet<TargetLister.Target>();

    Collection<org.apache.tools.ant.module.api.support.TargetLister.Target> targetsToUse = null;
    try {
      targetsToUse = TargetLister.getTargets(getProjectCookie(document, offset));
    } catch (IOException ex) {
    }

    if (targetsToUse != null) {
      for (final TargetLister.Target target : targetsToUse) {
        final Element targetsElement = target.getElement();
        final Attr attributeNode = targetsElement.getAttributeNode(STRING_DEPENDS);
        if (attributeNode != null && getSpanForString(document, attributeNode.toString(), offset) != null) {
          // found the target we are in - iterate on all targets again to find those already used in the depends
          final String dependsString = attributeNode.toString();
          for (final TargetLister.Target evalTarget : targetsToUse) {
            if (dependsString.contains(evalTarget.getQualifiedName()) || dependsString.contains(evalTarget.getName())) {
              usedTargets.add(evalTarget);
            }
          }
          break;
        }
      }
    }
    return usedTargets;
  }

  /**
   * Gets the span for the passed target in the passed document
   * @param document
   * @param target
   * @return the span for the passed target in the passed document or {@code null} if it wasn't available
   */
  public static int[] getSpanForTarget(final Document document, final TargetLister.Target target) {
    int[] ret = null;
    String text = null;
    try {
      text = document.getText(0, document.getLength());
    } catch (BadLocationException ex) {
      Logger.getLogger("AntParser").log(Level.INFO, "Cannot find given location ({0})", document.getLength()); // NOI18N
    }

    if (text != null && text.length() > 0) {
      document.getDefaultRootElement();
    }

    return ret;
  }

  private static boolean isInsideDepends(final org.apache.tools.ant.module.api.support.TargetLister.Target target,
          final Document document, final int offset) {
    boolean retValue = false;

    final Element targetsElement = target.getElement();
    final Attr attributeNode = targetsElement.getAttributeNode(STRING_DEPENDS);
    if (attributeNode != null) {
      final int[] spanForString = getSpanForString(document, attributeNode.toString(), offset);
      if (spanForString != null) {
        try {
          final String use = document.getText(spanForString[0], spanForString[1] - spanForString[0]);
          int indexOfStartQuote = use.indexOf('\"');
          int indexOfEndQuote = use.indexOf('\"', indexOfStartQuote + 1);

          // correct the offsets of the quote signs to the values corresponding to the document
          indexOfStartQuote = spanForString[0] + indexOfStartQuote;
          indexOfEndQuote = spanForString[0] + (indexOfEndQuote < 0 ? document.getLength() : indexOfEndQuote);

          retValue = offset > indexOfStartQuote && offset <= indexOfEndQuote;

        } catch (BadLocationException ex) {
        }
      }
    }

    return retValue;
  }

  private static boolean isInsideAntcall(final TargetLister.Target target, final Document document, final int offset) {
    boolean retValue = false;

    // TODO: go on here
    final Node antCallNode = getTaskNodeContainingOffset(document, offset, target);
    if (antCallNode != null) {
      Node nodeWithTarget = antCallNode.hasAttributes() ? antCallNode.getAttributes().getNamedItem("target") : null;
      if (nodeWithTarget == null) {
        // the target is not given as attribute (<antcall target="..."/>, but is <antcall><target name="..."/> </antcall>)
        final NodeList childNodes = antCallNode.getChildNodes();
        for (int index = 0; index < childNodes.getLength(); index++) {
          final Node child = childNodes.item(index);
          if ("target".equals(child.getNodeName().toLowerCase()) && child.hasAttributes()
                  && child.getAttributes().getNamedItem("name") != null) {
            nodeWithTarget = child.getAttributes().getNamedItem("name");
            break;
          }
        }
      }
      if (nodeWithTarget != null) {
        // target is given as attribute
        final int[] spanForString = getSpanForString(document, nodeWithTarget.toString(), offset);
        if (spanForString != null) {
          try {
            final String use = document.getText(spanForString[0], spanForString[1] - spanForString[0]);
            int indexOfStartQuote = use.indexOf('\"');
            int indexOfEndQuote = use.indexOf('\"', indexOfStartQuote + 1);

            // correct the offsets of the quote signs to the values corresponding to the document
            indexOfStartQuote = spanForString[0] + indexOfStartQuote;
            indexOfEndQuote = spanForString[0] + (indexOfEndQuote < 0 ? document.getLength() : indexOfEndQuote);

            retValue = offset > indexOfStartQuote && offset <= indexOfEndQuote;

          } catch (BadLocationException ex) {
          }
        }
      }
    }

    return retValue;
  }

  /**
   * Indicates whether the passed offset is inside a supported element of the Ant file. Supported are
   * <ul>
   * <li>"depends" attribute and target definition</li>
   * <li>"target" attribute or elemt of task "ant"</li>
   * <li>"target" attribute or element of task "antcall"</li>
   * </ul>
   * {@code Inside} then means to be in the quotes, e.g. depends=x"" is outside, but depends="x" is inside (where x
   * marks the offset.
   * @param target the target to look up (if none is given, all targets of the document are used
   * @param document the document to look in
   * @param offset the offset to check
   * @return {@code true} if the offset is inside a supported element
   */
  public static boolean isInsideSupportedElement(final org.apache.tools.ant.module.api.support.TargetLister.Target target,
          final Document document, final int offset) {
    boolean retValue = false;
    Collection<org.apache.tools.ant.module.api.support.TargetLister.Target> targetsToUse = null;
    if (target == null) {
      try {
        targetsToUse = TargetLister.getTargets(getProjectCookie(document, offset));
      } catch (IOException ex) {
      }
    } else {
      targetsToUse = Collections.singleton(target);
    }
    if (targetsToUse != null) {
      for (final TargetLister.Target targetToCheck : targetsToUse) {
        final Element targetElement = targetToCheck.getElement();
        final Attr attributeNode = targetElement.getAttributeNode(STRING_DEPENDS);
        if (attributeNode != null) {
          final int[] spanForString = getSpanForString(document, attributeNode.toString(), offset);
          if (spanForString != null) {
            try {
              final String use = document.getText(spanForString[0], spanForString[1] - spanForString[0]);
              int indexOfStartQuote = use.indexOf('\"');
              int indexOfEndQuote = use.indexOf('\"', indexOfStartQuote + 1);

              // correct the offsets of the quote signs to the values corresponding to the document
              indexOfStartQuote = spanForString[0] + indexOfStartQuote;
              indexOfEndQuote = spanForString[0] + (indexOfEndQuote < 0 ? document.getLength() : indexOfEndQuote);

              retValue = offset > indexOfStartQuote && offset < indexOfEndQuote;

            } catch (BadLocationException ex) {
            }
          }
        }
      }
    }

    return retValue;
  }

  /**
   * Tries to get the span from the depend tag of the passed target
   * @param target the target to look in
   * @param text the whole text
   * @param offset the text offset the mouse pointer is located currently
   * @return the span (start offset to end offset) or null if mouse is not pointing to depend
   */
  // XXX: use ANT targets here - would be better
  private static int[] getSpanFromDepends(final org.apache.tools.ant.module.api.support.TargetLister.Target target,
          final Document document, final int offset) {
    return getSpan(target.getElement().getAttributeNode(STRING_DEPENDS), document, offset);
  }

  private static int[] getSpanFromAntTask(final org.apache.tools.ant.module.api.support.TargetLister.Target target,
          final Document document, final int offset, final String taskName) {
    int result[] = null;

    final NodeList antTaskCalls = target.getElement().getElementsByTagName(taskName);
    // iterate through the list of tasks defined in the passed target
    for (int index = 0; index < antTaskCalls.getLength() && result == null; index++) {
      final Node taskCall = antTaskCalls.item(index);
      // get the target attribute
      final Node targetAttribute = taskCall.getAttributes().getNamedItem("target");

      if (targetAttribute == null) {
        // no target attribute?
        // iterate over through the child nodes
        String docsText = null;
        try {
          docsText = document.getText(0, document.getLength());
        } catch (BadLocationException ex) {
          Logger.getLogger("AntParser").log(Level.INFO, "Cannot find given location ({0})", document.getLength()); // NOI18N
        }
        for (int childNo = 0; childNo < taskCall.getChildNodes().getLength() && result == null; childNo++) {
          final Node child = taskCall.getChildNodes().item(childNo);
          if ("target".equals(child.getNodeName().toLowerCase()) && child.hasAttributes()) {
            final Node childsAttribute = child.getAttributes().getNamedItem("name");
            if (childsAttribute != null) {
              result = getSpan(childsAttribute, document, offset);
              // dirty hack to fix a problem with "property" and "target" in task "ant"
              // both defines an attribute name and the getSpan(...) method can not decide correctly
              if (result != null && docsText != null) {
                // 1. find the last position of "target" before the span start
                int indexOfTarget = docsText.indexOf("target");
                while (indexOfTarget > -1) {
                  if (docsText.indexOf("target", indexOfTarget + 1) > result[0]) {
                    break;
                  }
                  indexOfTarget = docsText.indexOf("target", indexOfTarget + 1);
                }
                // find the next closing bracket
                final int nextClosing = docsText.indexOf('>', indexOfTarget);
                if (nextClosing > -1 && nextClosing < result[0]) {
                  result = null;
                }
              }
            }
          }
        }
      } else {
        result = getSpan(targetAttribute, document, offset);
      }
    }
    return result;
  }

  private static int[] getSpan(final Node attributeNode, final Document document, final int offset) {
    int result[] = null;
    if (attributeNode != null) {
      int[] spanForString = getSpanForString(document, attributeNode.toString(), offset);
      if (spanForString != null) {
        try {
          final String value = attributeNode.getNodeValue();
          // handle the problem of not having a value (value="")
          String origText = document.getText(spanForString[0], spanForString[1] - spanForString[0]);
          String use = origText;
          // first correct the span value
          spanForString[0] += use.indexOf(value);
          // correct the used string, to be able to use a string tokenizer
          use = use.substring(use.indexOf(value), use.indexOf(value) + value.length());
          if (use.length() > 0) {
            final StringTokenizer tokenizer = new StringTokenizer(use, ",");
            int startIndex = 0;
            while (tokenizer.hasMoreElements()) {
              final String token = tokenizer.nextToken().trim();
              // use a startIndex here, as we can have the same name several times
              // e.g. TWO.b1_t1,b1_t1 will always give a token index of 0 when looking for b1_t1 only
              // what will lead to a non-result
              final int tokenIndex = use.indexOf(token, startIndex);
              startIndex = tokenIndex + token.length() + 1;
              if (spanForString[0] + tokenIndex <= offset && spanForString[0] + tokenIndex + token.length() >= offset) {
                result = new int[]{spanForString[0] + tokenIndex, spanForString[0] + tokenIndex + token.length()};
                break;
              }
            }
          } else {
            final int startQuote = origText.indexOf('\"');
            if (startQuote > -1) {
              final int endQuote = origText.indexOf('\"', startQuote + 1);
              if (endQuote > -1) {
                result = new int[]{startQuote, endQuote};
              }
            }
          }
        } catch (BadLocationException ex) {
        }
      }
    }
    return result;
  }

  private static int[] getSpanForString(final Document document, final String forString, final int offset) {
    int[] result = null;

    if (forString != null && forString.length() > 0) {
      final int stringLength = forString.length();
      try {
        if (stringLength > 0) {
          final int start = (offset - stringLength > 0) ? offset - stringLength : 0;
          final int length = (start + 2 * stringLength) >= document.getLength() ? document.getLength() - start : 2 * stringLength;
          if (length > 0) {
            final String usedText = document.getText(start, length);
            final int startOfString = usedText.indexOf(forString);
            if (startOfString > -1 && start + startOfString <= offset && start + startOfString + stringLength >= offset) {
              result = new int[]{start + startOfString, start + startOfString + stringLength};
            }
          }
        }

      } catch (BadLocationException ex) {
      }
    }
    return result;
  }

  private static Set<org.apache.tools.ant.module.api.support.TargetLister.Target> getTargets(final Document document) {
    final Set<org.apache.tools.ant.module.api.support.TargetLister.Target> result = new HashSet<org.apache.tools.ant.module.api.support.TargetLister.Target>();

    final AntProjectCookie projectCookie = getProjectCookie(document);
    if (projectCookie != null) {
      try {
        result.addAll(TargetLister.getTargets(projectCookie));
      } catch (IOException ex) {
      }
    }

    return result;
  }

  /**
   * Gets the file the passed target is defined in according to the build order.
   * @param document
   * @param targetName
   * @param offset
   * @return
   */
  public static File getFileForTargetName(final Document document, final String targetName, final int offset) {
    File retValue = null;

    Project project = setupProject(document, null);
    // this is a bit tricky - first we try, the offset is inside a task of type "ant"
    // if not, we can just get the full build order, as then the offset can be in an "depends" or "antcall" only
    // which always refer to the same build project
    if (project != null) {
      for (final org.apache.tools.ant.module.api.support.TargetLister.Target target : getTargets(document)) {
        final Node result = getTaskNodeContainingOffset(document, offset, target);
        if (result != null) {
          final Node namedItem = result.getAttributes().getNamedItem("antfile");
          final Node pathItem = result.getAttributes().getNamedItem("dir");
          // may there's an error, so better check the existence of the attribute
          if (namedItem != null) {
            String fileName = project.replaceProperties(namedItem.getNodeValue());
            if (fileName != null && fileName.length() > 0) {
              String path = "";
              if (pathItem != null && pathItem.getNodeValue() != null) {
                path = project.replaceProperties(pathItem.getNodeValue());
              }

              if (path.length() > 0) {
                fileName = path + "" + File.separatorChar + "" + fileName;
              }

              final File resolvedFile = project.resolveFile(fileName);
              project = setupProject(resolvedFile, null);
              break;
            }
          }
        }
      }

      if (retValue == null) {
        for (final org.apache.tools.ant.Target target : getSupposedBuildOrder(project, "")) {
          if (target.getName().equals(targetName)) {
            retValue = new File(target.getLocation().getFileName());
            break;
          }
        }
      }
    }
    return retValue;
  }

  /**
   * Gets the file for the given property name. Will return a value if and only if the property is defined in a real
   * file.
   * @param document the document the property is defined in
   * @param propertyName the property to get the file for
   * @param offset the offset used
   * @return
   */
  public static File getFileForProperty(final Document document, final String propertyName) {
    final PropertiesInterceptorListener listener = new PropertiesInterceptorListener(propertyName);
    setupProject(document, listener);
    return listener.getFile();
  }

  private static final class PropertiesInterceptorListener implements BuildListener {
    private final String propertyName;
    private File file = null;
    private boolean found = false;

    protected PropertiesInterceptorListener(final String propertyName) {
      this.propertyName = propertyName;
    }

    public void buildStarted(BuildEvent be) {
    }

    public void buildFinished(BuildEvent be) {
    }

    public void targetStarted(BuildEvent be) {
    }

    public void targetFinished(BuildEvent be) {
    }

    public void taskStarted(BuildEvent be) {
    }

    public void taskFinished(BuildEvent be) {
      found = file != null && be.getProject().getProperty(propertyName) != null;
    }

    public void messageLogged(BuildEvent be) {
      if (!found && be.getTask() != null && be.getTask() instanceof Property) {
        final Property task = ((Property) be.getTask());
        file = task.getFile();
      }
    }

    public File getFile() {
      return file;
    }
  }

  /**
   * Looks up all the nodes of the passed targets and finds the node, which contains the passed offset.
   * @param document
   * @param offset
   * @param target
   * @param taskName
   * @return
   */
  private static Node getTaskNodeContainingOffset(final Document document, final int offset,
          final org.apache.tools.ant.module.api.support.TargetLister.Target target) {
    Node result = null;
    final NodeList antTaskCalls = target.getElement().getChildNodes();

    // iterate through the list of tasks defined in the passed target
    for (int index = 0; index < antTaskCalls.getLength() && result == null; index++) {
      final Node taskCall = antTaskCalls.item(index);
      // get the target attribute
      final Node targetAttribute = taskCall.hasAttributes() ? taskCall.getAttributes().getNamedItem("target") : null;

      if (targetAttribute == null) {
        // no target attribute?
        // iterate over through the child nodes
        String docsText = null;
        try {
          docsText = document.getText(0, document.getLength());
        } catch (BadLocationException ex) {
          Logger.getLogger("AntParser").log(Level.INFO, "Cannot find given location ({0})", document.getLength()); // NOI18N
        }
        for (int childNo = 0; childNo < taskCall.getChildNodes().getLength() && result == null; childNo++) {
          final Node child = taskCall.getChildNodes().item(childNo);
          if ("target".equals(child.getNodeName().toLowerCase()) && child.hasAttributes()) {
            final Node childsAttribute = child.getAttributes().getNamedItem("name");
            if (childsAttribute != null) {
              final int[] span = getSpan(childsAttribute, document, offset);
              // dirty hack to fix a problem with "property" and "target" in task "ant"
              // both defines an attribute name and the getSpan(...) method can not decide correctly
              if (span != null && docsText != null) {
                // 1. find the last position of "target" before the span start
                int indexOfTarget = docsText.indexOf("target");
                while (indexOfTarget > -1) {
                  if (docsText.indexOf("target", indexOfTarget + 1) > span[0]) {
                    break;
                  }
                  indexOfTarget = docsText.indexOf("target", indexOfTarget + 1);
                }
                // find the next closing bracket
                final int nextClosing = docsText.indexOf('>', indexOfTarget);
                // refactor -> reads strange and is probably wrong
                if (nextClosing > -1 && nextClosing < span[0]) {
                  result = null;
                } else {
                  result = taskCall;
                }
              }
            }
          }
        }
      } else {
        if (getSpan(targetAttribute, document, offset) != null) {
          result = taskCall;
        }
      }
    }
    return result;
  }

  /**
   * Gets the supposed build order for the target with the passed name.
   * @param document the document to look in
   * @param targetName the target's name to get the build order for
   * @return the supposed build order up to the given target (excluding). If passed target name is empty or null, the
   * complete build order as provided by ANT will be returned. In any case the returned list in non-null.
   */
  public static List<org.apache.tools.ant.Target> getSupposedBuildOrder(final Document document, final String targetName) {
    return getSupposedBuildOrder(setupProject(document, null), targetName);
  }

  /**
   * Gets the supposed build order for the target with the passed name in the document the passed offset references to.
   * @param document the document to look in
   * @param targetName the target's name to get the build order for
   * @param offset the offset in the passed document
   * @return the supposed build order up to the given target (excluding). If passed target name is empty or null, the
   * complete build order as provided by ANT will be returned. In any case the returned list in non-null.
   */
  public static List<org.apache.tools.ant.Target> getSupposedBuildOrder(final Document document, final String targetName, int offset) {
    return getSupposedBuildOrder(setupProject(document, offset, null), targetName);
  }

  @SuppressWarnings("UseOfObsoleteCollectionType")
  private static List<org.apache.tools.ant.Target> getSupposedBuildOrder(final Project project, final String targetName) {
    final List<org.apache.tools.ant.Target> retList = new ArrayList<org.apache.tools.ant.Target>();
    if (project != null) {
      final String tName = targetName == null ? "" : targetName;
      @SuppressWarnings("unchecked")
      List<org.apache.tools.ant.Target> topoSort = null;
      try {
        topoSort = project.topoSort(tName, project.getTargets());
      } catch (BuildException buildEx) {
        // may the target is not defined - so we have to get all the targets unsorted
        final Hashtable targets = project.getTargets();
        topoSort = new ArrayList<Target>();
        for (Object object : targets.keySet()) {
          topoSort.add((Target) targets.get(object));
        }
      }

      for (final org.apache.tools.ant.Target target : topoSort) {
        if (!"".equals(tName) && target.getName().equals(tName)) {
          break;
        } else {
          retList.add(target);
        }
      }
    }
    return retList;
  }

  /**
   * Gets the location for target with passed name in the passed file. Note, that if the file does not define the target,
   * but an imported file do so, the location inside the imported file will be returned.
   * @param file the file to initial look up in
   * @param targetName the name of the target to find the location for
   * @return the location if found or {@code null}
   */
  public static Location getLocation(final File file, final String targetName) {
    Location retValue = null;

    if (file != null) {
      final Project project = setupProject(file, null);
      if (project != null) {
        org.apache.tools.ant.Target target = (Target) project.getTargets().get(targetName);
        // not found? try again - may it's the qualified name
        if (target == null && targetName.contains(".")) {
          target = (Target) project.getTargets().get(targetName.substring(targetName.indexOf('.') + 1));
        }

        if (target != null) {
          retValue = target.getLocation();
        }
      }
    }
    return retValue;
  }

  /**
   * Resolves the properties inside the given string (if any) for the project the passed document belongs to.
   * @param document the document the passed string belongs to
   * @param string the string to resolve the properties
   * @return a string containing the resolved properties
   */
  public static String resolveProperties(final Document document, final String string) {
    final Project project = setupProject(document, null);
    return project.replaceProperties(string);
  }

  private static Project setupProject(final Document document, BuildListener listener) {
    Project project = null;
    final AntProjectCookie projectCookie = getProjectCookie(document);

    if (projectCookie != null) {
      final File buildFile = FileUtil.toFile(projectCookie.getFileObject());
      project = setupProject(buildFile, listener);
    }

    return project;
  }

  /**
   * Sets up a project which may isn't related to the passed document, but to a document referenced by the task the
   * passed offset is in.
   * @param document the document containing the passed offset position
   * @param offset the offset itself
   * @return the project according to the document the task at the passed offset refers to
   */
  private static Project setupProject(final Document document, int offset, final BuildListener listener) {
    Project project = null;
    final AntProjectCookie projectCookie = getProjectCookie(document, offset);

    if (projectCookie != null) {
      final File buildFile = FileUtil.toFile(projectCookie.getFileObject());
      project = setupProject(buildFile, listener);
    }

    return project;
  }

  public static Project setupProject(final File file, final BuildListener listener) {
    Project project = null;
    if (file != null && isAntFile(FileUtil.toFileObject(file))) {
      project = new Project();
      if(listener != null) {
        project.addBuildListener(listener);
      }
      project.setCoreLoader(null);
      project.init();

      project.setUserProperty(MagicNames.ANT_FILE, file.getAbsolutePath());
      try {
        ProjectHelper.configureProject(project, file);
      } catch (Exception ex) {
        Logger.getLogger("AntParser").log(Level.INFO, ex.getMessage()); // NOI18N
      }
      project.init();
    }

    return project;
  }

  /**
   * Gets the project cookie for the passed document.
   * @param document the document to get the cookie for
   * @return the project cookie of the passed document or {@code null} if none could be found
   */
  public static AntProjectCookie getProjectCookie(final Document document) {
    AntProjectCookie retValue = null;
    if (document != null) {
      final FileObject fileObject = NbEditorUtilities.getFileObject(document);
      if (isAntFile(fileObject)) {
        retValue = AntScriptUtils.antProjectCookieFor(fileObject);
      }
    }
    return retValue;
  }

  /**
   * Gets the project cookie for the project the task at the passed offset in the passed document refers to.
   * <p>Example
   * <pre>
   *   <target name="haha">
   *     <ant antfile="someOtherBuildFile.xml" target="someTarget"/>
   *   </target>
   * </pre>
   * In the above example the target "someTarget" in another file then the original used one must be evaluated (e.g.
   * for completion).
   * @param document the document to get the cookie for
   * @param offset the offset in the document. If {@code <0} it's assumed to return the cookie for the
   * passed document
   * @return the project cookie of the passed document or {@code null} if none could be found
   */
  public static AntProjectCookie getProjectCookie(final Document document, int offset) {
    AntProjectCookie retValue = null;
    if (document != null) {
      final File fileForTargetName = getFileForTargetName(document, "", offset);
      final FileObject fileObject = fileForTargetName == null || !fileForTargetName.exists()
              ? NbEditorUtilities.getFileObject(document)
              : FileUtil.toFileObject(fileForTargetName);
      if (isAntFile(fileObject)) {
        retValue = AntScriptUtils.antProjectCookieFor(fileObject);
      }
    }
    return retValue;
  }
}
